<?php

namespace Aoe\Database;

use Aoe\Emulator\Ifc\Type;
use Exception;
use PDO;
use Throwable;

/**
 * # 数据库适配器
 *   map: 运算语句
 *   use: 操作语句
 */
abstract class Platform
{
    /**
     * 错误信息
     */
    const string ERR_CONNECT = '由于${err}数据库${dsn}连接失败';
    
    protected(set) PDO $pdo {
        get {
            return $this->pdo;
        }
    }

    /**
     * @param array $config
     *
     * @throws Exception
     */
    public function __construct(array $config)
    {
        $this->pdo = $this->_connect_($config);
    }

    /**
     * @param array{ user?: string, pass?: string, charset?: string, driverOptions?: array } $params
     *
     * @return PDO
     * @throws Exception
     */
    private function _connect_(array $params): PDO
    {
        $dsn = $this->make_dsn($params);
        
        try {
            $driverOptions                    = $params['driverOptions'] ?? [];
            $driverOptions[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;
            
            $pdo = new PDO($dsn, $params['user'] ?? null, $params['pass'] ?? null, $driverOptions);
            
            $this->on_connected($pdo, $params);
            $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            
            return $pdo;
        } catch (Throwable $e) {
            throw new \Aoe\Util\Exception(self::ERR_CONNECT, ['dsn' => $dsn, 'err' => $e->getMessage()]);
        }
    }
    
    /**
     * @param array{ name?: string, file?: string, type?: string, host?: string, port?: string, unix_socket?: string, charset?: string } $config 数据库参数
     *
     * @return string
     */
    abstract protected function make_dsn(array $config): string;
    
    protected function on_connected(Pdo $pdo, array $params): void
    {
        $charset = 'SET NAMES ' . $params['charset'] ?? 'utf8';
        $pdo->query($charset);
    }

    /**
     * 字符串值转义处理
     *
     * @param string $column
     *
     * @return string
     */
    public function quote(string $column): string
    {
        try {
            $r = $this->pdo->quote($column);
            if ($r !== false) return $r;
        } catch (Throwable) {}

        return '\'' . str_replace('\'', '', $column) . '\'';
    }

    public function mapColumns(array| string $columns): string
    {
        return is_array($columns) ? implode(',', array_map($this->mapColumn(...), $columns)) : $columns;
    }

    public function mapColumn(string $column): string
    {
        return '`' . str_replace('`', '', $column) . '`';
    }

    /**
     * 生成 IN 语句
     *
     * @param string $column  字段名
     * @param array  $values 值序列
     * @param int    $type   值类型
     *
     * @return string|bool
     * @throws Exception
     */
    public function mapIn(string $column, array $values, int $type = PDO::PARAM_INT): string | bool
    {
        return $this->mapNotIn($column, $values, $type, 'IN');
    }
    
    /**
     * 生成 NOT IN 语句
     *
     * @param string $column  字段名
     * @param array  $values 值序列
     * @param int    $type   值类型
     * @param string $op
     *
     * @return string|bool
     * @throws Exception
     */
    public function mapNotIn(string $column, array $values, int $type = PDO::PARAM_INT, string $op = 'NOT IN'): string | bool {
        if (empty($values)) return false;
        
        $quote = function ($n) use ($type) {
            if ($type === PDO::PARAM_STR) {
                return $this->quote($n);
            } else {
                return (int)$n;
            }
        };
        
        if (count($values) === 1) {
            $v = $quote($values[0]);
            $O = $op === 'IN' ? '=' : '!=';
            return "$column $O $v";
        }
        
        $ids = implode(',', array_map($quote, $values));
        return "$column $op ($ids)";
    }

    /**
     * 生成 BETWEEN 语句
     *
     * @param string $column
     * @param array $v
     * @return string
     */
    public function mapBetween(string $column, array $v): string
    {
        return "$column BETWEEN $v[0] AND $v[1]";
    }
    
    public function mapLike(string $value): bool | string
    {
        if ($value[0] === '%') $value = substr($value, 1);
        else $value = "%$value%";
        
        return $this->quote($value);
        
    }
    
    /**
     * concat语句
     *
     * @param string                       $column
     * @param string|array{string, string} $value
     *
     * @return string
     * @throws Exception
     */
    public function mapConcat(string $column, string | array $value): string
    {
        if (is_array($value)) {
            $prefix = $this->quote($value[0]);
            $suffix = $this->quote($value[1]);
        } else {
            $prefix = '';
            $suffix = $this->quote($value);
        }
        return "$column=CONCAT($prefix, $column, $suffix)";
    }
    
    /**
     * Replace语句
     *
     * @param string                         $column
     * @param string | array{string, string} $value
     *
     * @return string
     * @throws Exception
     */
    public function mapReplace(string $column, string | array $value): string
    {
        if (is_array($value)) {
            $from = $this->quote($value[0]);
            $to   = $this->quote($value[1]);
        } else {
            $from = $this->quote($value);
            $to   = '';
        }
        return "$column=REPLACE($column, $from, $to)";
    }
    
    public function mapOpposite($value): int
    {
        if (!is_int($value)) return 1;
        
        return ~$value;
    }
    
    public function useIdSql(bool $uuid): string
    {
        return $uuid ?
            '   id INT PRIMARY KEY NOT NULL,' :
            '   id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,';
    }
    
    public function useDefaultValueSql($value): string
    {
        return "DEFAULT $value";
    }
    
    public function useCommentSql(array $obj): string
    {
        $comment = $obj['comment'] ?? ($obj['label'] ?? false);
        
        if (!$comment) return '';
        
        return "COMMENT '$comment'";
    }
    
    public function useStatusSql(): string
    {
        $str = Type::STATUS;
        return "  $str BIT DEFAULT b'1' NOT NULL";
    }
    
    public function useAdminSql(): string
    {
        $str = Type::OWNER;
        return "  $str INT";
    }
    
    public function useCTSql(): string
    {
        $str = Type::CREATE;
        return "  $str INT";
    }
    public function useUTSql(): string
    {
        $str = Type::UPDATE;
        return "  $str INT";
    }
    
    /**
     * @param array{ label: string, comment?: string, name: string } $table
     * @param bool                                     $simple
     *
     * @return string
     */
    public function useAfterSql(array $table, bool $simple): string
    {
        $auto = ($simple) ? '' : 'AUTO_INCREMENT=1';
        
        $comment = $table['comment'] ?? $table['label'] ?? $table['name'] ?? '';

        return ")ENGINE=InnoDB $auto DEFAULT CHARSET=utf8 COMMENT='$comment';";
    }
    
    public function canGroup(): bool
    {
        return true;
    }
    
    public function useIntString(): string
    {
        return 'INT';
    }
    
    public function useBitString(): string
    {
        return 'BIT(1)';
    }
    
    public function useUnsigned(): string
    {
        return ' UNSIGNED';
    }
    
    public function mapCbl(string $column, mixed $param): string
    {
        $v = Platform::int_2_birth($param);
        return "(SUBSTRING($column, 7, 6) + 0) < $v";
    }
    
    public function mapCbg(string $column, mixed $param): string
    {
        $v = Platform::int_2_birth($param);
        return "(SUBSTRING($column, 7, 6) + 0) > $v";
    }

    protected static function int_2_birth(int $v): int
    {
        return (int)(date_create()->setTimestamp($v)->format('Ym'));
    }

}